﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Windows;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Security;
using System.IO.Compression;

namespace Samcrypt
{
    public class Tester
    {
        public static void encoderTest1()
        {
            string testStr = "string to encode";
            byte[] testStrBytes = Encoding.Default.GetBytes(testStr);

            MemoryStream memStream = new MemoryStream();
            Base64Encoder base64Encoder = new Base64Encoder(new StreamWriter(memStream), true);
            base64Encoder.Write(testStrBytes, 0, testStrBytes.Length);
            base64Encoder.Close();
            memStream.Seek(0, SeekOrigin.Begin);
            StreamReader streamReader = new StreamReader(memStream);

            string encodedStr = streamReader.ReadToEnd().TrimEnd();

            string bclEncodedStr = Convert.ToBase64String(testStrBytes);

            bool eqiv = bclEncodedStr == encodedStr;
            Console.WriteLine(eqiv);
            Debug.Assert(eqiv);

            string decodedStr = Encoding.Default.GetString(Convert.FromBase64String(encodedStr));
            eqiv = testStr == decodedStr;
            Console.WriteLine(eqiv);
            Debug.Assert(eqiv);

        }

        public static void Run()
        {
            //Run2(40 * 1077);
            //return;


            encoderTest1(); return;


            //StringBuilder sbText = new StringBuilder();
            //for (int i = 0; i < 10000; ++i)
            //    sbText.Append(string.Format("This is my world hello world baw {0}", (i * 7).ToString()));

            //string testString = sbText.ToString();
            //byte[] testStrBytes = Encoding.Default.GetBytes(testString);
            //string testStrBase64 = Convert.ToBase64String(testStrBytes);


            //StringBuilder sb = new StringBuilder();
            //sb.AppendLine("    ");
            //sb.AppendLine("");
            //sb.AppendLine("   \n\n");
            //sb.AppendLine("   \n# hellow world");
            //sb.AppendLine("  # hellow world");
            //sb.Append(testStrBase64);


            //MemoryStream memStream = new MemoryStream(Encoding.Default.GetBytes(sb.ToString()));
            //StreamReader streamReader = new StreamReader(memStream);
            //Base64Decoder base64Decoder = new Base64Decoder(streamReader);
            //StreamReader streaReader2 = new StreamReader(base64Decoder);

            //long start;

            //start = DateTime.Now.Ticks;
            //string outString = streaReader2.ReadToEnd();
            //long delta1 = DateTime.Now.Ticks - start;

            //start = DateTime.Now.Ticks;
            //byte[] plainTextBytes = Convert.FromBase64String(testStrBase64);
            //string stringPlainText = Encoding.Default.GetString(plainTextBytes);
            //long delta2 = DateTime.Now.Ticks - start;

            //Console.WriteLine("Delta1 = {0}\nDelta2={1}", delta1, delta2);
            //double multDif = (double)delta1 / (double)delta2;
            //MessageBox.Show(string.Format("Delta1 = {0}\nDelta2={1} | multDif={2}", delta1, delta2, multDif));
            //Console.WriteLine("{0}", multDif);


            //bool cmp = outString == testString;
            //Debug.Assert(cmp);



        }

        public static void Run2(int byteCount)
        {
            Rfc2898DeriveBytes deriveBytes = new Rfc2898DeriveBytes("no_sec", 32, 2);
            byte[] testBytes = deriveBytes.GetBytes(byteCount);

            string testBytesBase64 = Convert.ToBase64String(testBytes);

            MemoryStream memStream = new MemoryStream(Encoding.Default.GetBytes(testBytesBase64));
            StreamReader streamReader = new StreamReader(memStream);
            Base64Decoder base64Decoder = new Base64Decoder(streamReader);

            // todo: small buffer
            byte[] buffer = new byte[byteCount];
            int bytesWritten = base64Decoder.Read(buffer, 0, buffer.Length);

            if (CryptoHelper.CompareBytes(testBytes, buffer))
                Console.WriteLine("Test Passed");
            else
                Console.WriteLine("Test FAILED");
        }
    }

    #region Clipbaord Reader and Writer

    public class ClipboardReader : StreamReaderBase
    {

        public ClipboardReader()
            : base(null, 0)
        {

        }

        public override int Read(byte[] dest, int offset, int count)
        {
            
            if (this.buffer == null)
            {
                if (!Clipboard.ContainsText())
                    return 0;

                buffer = Encoding.Default.GetBytes(Clipboard.GetText());
            }
            else if (bufferPos >= buffer.Length)
            {
                return 0;
            }

            if (bufferPos + count > buffer.Length)
                count = buffer.Length - bufferPos; // set count to bytes remaning

            Buffer.BlockCopy(buffer, bufferPos, dest, offset, count);
            bufferPos += count;

            return count;
        }
    }

    public class ClipboardWriter : StreamWriterBase
    {
        static readonly int DefaultBufferLength = 1024;
        
        
        byte[] clipboardBuffer;
        int bufferPos;

        public ClipboardWriter() : this(DefaultBufferLength) { }
        
        public ClipboardWriter(int bufferLength)
            : base(null)
        {
            clipboardBuffer = new byte[bufferLength];
        }

        private void growBuffer()
        {
            byte[] oldBuffer = clipboardBuffer;
            clipboardBuffer = new byte[oldBuffer.Length * 2];

            Buffer.BlockCopy(oldBuffer, 0, clipboardBuffer, 0, bufferPos);
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (bufferPos + count >= clipboardBuffer.Length)
                growBuffer();

            Buffer.BlockCopy(buffer, 0, clipboardBuffer, bufferPos, count);
            bufferPos += count;
        }

        public override void Close()
        {
            Clipboard.SetText(Encoding.Default.GetString(clipboardBuffer, 0, bufferPos));
        }

        public override void Flush()
        {
        }

    }

    #endregion

    #region Base64 Encoder and Decoder
    
    public class Base64Encoder : Stream
    {
        TextWriter textWriter;


        char[] charBuffer = new char[4 * 19];
        int charBufferPos;

        byte[] tempBytes = new byte[3];
        int tempBytesCount;

        bool finalized;
        bool keepBaseStreanOpen;

        public Base64Encoder(TextWriter textWriter, bool keepBaseStreanOpen)
        {
            this.textWriter = textWriter;
            this.keepBaseStreanOpen = keepBaseStreanOpen;
        }

   
        protected static readonly char[] lookup = new char[] { 
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            '+', '/', '=' };

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (finalized)
                throw new Exception("Can't write additional bytes after Flush() has been called");

            if (count <= 0)
                throw new Exception("Count is zero");  // TODO: determin if this happens / has meaning


            // 1.  If temp bytes exist, fill remainder and writeBlock
            if (tempBytesCount > 0)
            {

                do
                {
                    tempBytes[tempBytesCount++] = buffer[offset++];
                    --count;

                } while (tempBytesCount < 3 && count > 0);

                if (count <= 0) 
                    return;

                Debug.Assert(tempBytesCount == 3);
                writeBlock(tempBytes, 0);

                tempBytesCount = 0;
            }

            // 2.  Write blocks
            while (count >= 3)
            {
                writeBlock(buffer, offset);
                offset += 3;
                count -= 3;
            }

            // 3.  Handle any remaining bytes
            if (count > 0)
            {
                tempBytesCount = 0;
                do
                {
                    tempBytes[tempBytesCount++] = buffer[offset++];
                    --count;
                } while (count > 0);
            }
            

        }

        public override void Flush()
        {
            textWriter.Flush();
        }

        public override void Close()
        {
            
            if (tempBytesCount > 0)
            {
                writeBlock(tempBytes, 0, 3 - tempBytesCount);
                tempBytesCount = 0;
            }

            if (charBufferPos > 0)
            {
                // write out chars
                textWriter.WriteLine(charBuffer, 0, charBufferPos);
                charBufferPos = 0;
            }

            textWriter.Flush();
            if (!keepBaseStreanOpen)
                textWriter.Close();

            base.Close();
            finalized = true;
        }

        private void writeBlock(byte[] buffer, int offset)
        {
            if (charBufferPos >= charBuffer.Length)
            {
                // write out chars
                textWriter.WriteLine(charBuffer, 0, charBufferPos);
                charBufferPos = 0;
            }

            charBuffer[charBufferPos++] = lookup[buffer[offset] >> 2];
            charBuffer[charBufferPos++] = lookup[((buffer[offset++] & 0x03) << 4) | (buffer[offset] >> 4)];
            charBuffer[charBufferPos++] = lookup[((buffer[offset++] & 0x0f) << 2) | (buffer[offset] >> 6)];
            charBuffer[charBufferPos++] = lookup[buffer[offset++] & 0x3f];
        }
        private void writeBlock(byte[] buffer, int offset, int zeroBytes)
        {
            Debug.Assert(zeroBytes == 1 || zeroBytes == 2);

            if (charBufferPos >= charBuffer.Length)
            {
                // write out chars
                textWriter.WriteLine(charBuffer, 0, charBufferPos);
                charBufferPos = 0;
            }

            charBuffer[charBufferPos++] = lookup[buffer[offset] >> 2];
            charBuffer[charBufferPos++] = lookup[((buffer[offset++] & 0x03) << 4) | (buffer[offset] >> 4)];
            charBuffer[charBufferPos++] = zeroBytes == 2 ? '=' : lookup[((buffer[offset++] & 0x0f) << 2) | (buffer[offset] >> 6)];
            charBuffer[charBufferPos++] = zeroBytes == 2 || zeroBytes == 1 ? '=' : lookup[buffer[offset++] & 0x3f];

        }



        #region Constant Properties
        public override bool CanRead
        {
            get { return false; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }
        #endregion

        #region Not Implimented


        public override long Length
        {
            get { throw new NotImplementedException(); }
        }

        public override long Position
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }
        #endregion

    }

    public class Base64Decoder : Stream
    {
        TextReader textReader;
        static readonly int DefaultBufferLength = 512;

        char[] charBuffer;
        int charBufferPos = 0;
        int charBufferCount = 0;

        char curChar;
        byte curByte;

        int bytePos = 0;  // global byte pos (used to determin if byte is in 0, 1, or 2 place, where place = bytePos % 3)

        public Base64Decoder(TextReader textReader) : this(textReader, DefaultBufferLength) { }
        public Base64Decoder(TextReader textReader, int bufferLength)
        {
            this.textReader = textReader;
            charBuffer = new char[bufferLength];
        }

        private enum CharState { OK = 0, PadLastByte, PadLastTwoBytes, Done }

        bool eatLine()
        {
            for (; ; )
            {
                // iterate over buffer
                while (charBufferPos < charBufferCount)
                {
                    if (charBuffer[charBufferPos++] == '\n')
                        return true;
                }

                // get more chars
                if (!fillBuffer())
                    return false;
            }
        }

        bool eatWhiteSpace()
        {
            for (; ; )
            {
                // iterate over buffer
                while (charBufferPos < charBufferCount)
                {
                    if (!char.IsWhiteSpace(charBuffer[charBufferPos]))
                        return true;
                    ++charBufferPos;
                }

                // get more chars
                if (!fillBuffer())
                    return false;
            }

        }

        CharState lastCharState;

        CharState getNextValidChar()
        {


            for (; ; )
            {
                // iterate over buffer
                while (charBufferPos < charBufferCount)
                {
                    curChar = charBuffer[charBufferPos++];

                    if ('A' <= curChar && curChar <= 'Z')
                    {
                        curByte = (byte)(curChar - 'A');
                        lastCharState = CharState.OK;
                        goto returnCondition;
                    }
                    else if ('a' <= curChar && curChar <= 'z')
                    {
                        curByte = (byte)(26 + curChar - 'a');
                        lastCharState = CharState.OK;
                        goto returnCondition;
                    }
                    else if ('0' <= curChar && curChar <= '9')
                    {
                        curByte = (byte)(26 + 26 + curChar - '0');
                        lastCharState = CharState.OK;
                        goto returnCondition;
                    }
                    else if (curChar == '+')
                    {
                        curByte = (26 + 26 + 10);
                        lastCharState = CharState.OK;
                        goto returnCondition;
                    }
                    else if (curChar == '/')
                    {
                        curByte = (26 + 26 + 10 + 1);
                        lastCharState = CharState.OK;
                        goto returnCondition;
                    }
                    else if (curChar == '=')
                    {
                        curByte = 0;
                        if (charBufferPos >= charBufferCount)
                        {
                            if (!fillBuffer())
                            {
                                lastCharState = CharState.PadLastByte; // last char was '=' (no more chars)
                                goto returnCondition;
                            }
                            else
                            {
                                fillBuffer(1); // get one more char
                            }
                        }

                        // check next char for '=', i.e.: double '='
                        if ((curChar = charBuffer[charBufferPos++]) == '=')
                        {
                            lastCharState = CharState.PadLastTwoBytes;
                            goto returnCondition;
                        }
                        else
                        {
                            // strictly speaking this indicates invalid data. instead interpret as end of base64 data
                            //throw new Exception("Invalid Base64 text");
                            lastCharState = CharState.PadLastByte; // last char was '=' (no more chars)
                            goto returnCondition;
                        }
                    }
                    else if (char.IsWhiteSpace(curChar))
                    {
                        // eat white space
                        if (eatWhiteSpace())
                        {
                            continue;
                        }
                        else
                        {
                            lastCharState = CharState.Done;
                            goto returnCondition;
                        }
                    }
                    else if (curChar == '#')
                    {
                        // goto next line
                        if (eatLine())
                        {
                            continue;
                        }
                        else
                        {
                            lastCharState = CharState.Done;
                            goto returnCondition;
                        }
                    }
                    else
                    {
                        // invalid base64 char
                        throw new Exception("Invalid base64 char");
                    }
                }


                // fill char buffer    
                if (!fillBuffer())
                {
                    lastCharState = CharState.Done;
                    goto returnCondition;
                }

            }

        returnCondition:
            return lastCharState;

        }


        bool fillBuffer()
        {
            return fillBuffer(charBuffer.Length);
        }

        // returns true if one or mroe bytes filled;
        // returns false if no bytes filled
        bool fillBuffer(int maxBytes)
        {
            Debug.Assert(charBufferPos >= charBufferCount);

            int charsRead = textReader.Read(charBuffer, 0, maxBytes);

            charBufferCount = charsRead;
            charBufferPos = 0;

            return charsRead > 0;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (lastCharState != CharState.OK)
                return 0;

            int initBytePos = bytePos;

            for (int i = 0; i < count; ++i)
            {
                switch (bytePos % 3)
                {
                    case 0:
                        // byte 0
                        if (getNextValidChar() != CharState.OK)
                        {
                            if (lastCharState == CharState.Done)
                                goto returnCondition;
                            else //if (lastCharState == CharState.PadLastByte || lastCharState == CharState.PadLastByte)
                                throw new Exception("Mallformed base64 text");
                        }
                        buffer[offset] = (byte)(curByte << 2);           // [00AA | aaaa] --> [AAaa | aa00]  
                        if (getNextValidChar() != CharState.OK) throw new Exception("Mallformed base64 text");
                        buffer[offset++] |= (byte)(curByte >> 4);          // [00BB | bbbb] --> [0000 | 00BB] --> [AAaa | aaBB]
                        break;
                    case 1:
                        // byte 1
                        buffer[offset] = (byte)(curByte << 4);           // [00BB | bbbb] --> [bbbb | 0000] 
                        if (getNextValidChar() != CharState.OK)
                        {
                            if (lastCharState == CharState.PadLastTwoBytes)
                            {
                                buffer[offset] = 0;
                                goto returnCondition;
                            }
                            else //if (lastCharState == CharState.PadLastByte || lastCharState == CharState.Done)
                            {
                                throw new Exception("Mallformed base64 text");
                            }
                        }
                        buffer[offset++] |= (byte)(curByte >> 2);          // [00CC | cccc] --> [0000 | CCcc] --> [bbbb | CCcc]

                        break;
                    case 2:
                        // byte 2
                        buffer[offset] = (byte)(curByte << 6);           // [00CC | cccc] --> [cc00 | 0000] 
                        if (getNextValidChar() != CharState.OK)
                        {
                            if (lastCharState == CharState.PadLastByte)
                            {
                                buffer[offset] = 0;
                                goto returnCondition;
                            }
                            else //if (lastCharState == CharState.PadLastByte || lastCharState == CharState.Done)
                            {
                                throw new Exception("Mallformed base64 text");
                            }
                        }
                        buffer[offset++] |= (byte)(curByte >> 0);          // [00DD | dddd] --> [00DD | dddd] --> [ccDD | dddd]
                        break;
                }
                ++bytePos;

            }

        returnCondition:

            Debug.Assert(bytePos - initBytePos == offset);
            return offset;

        }

        #region Constant Properties
        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return false; }
        }
        #endregion

        #region Not Implimented

        public override void Flush()
        {
            throw new NotImplementedException();
        }

        public override long Length
        {
            get { throw new NotImplementedException(); }
        }

        public override long Position
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        #endregion
    }

    #endregion



    public abstract class StreamWriterBase : Stream
    {

        public Stream BaseStream { get; protected set; }

        public StreamWriterBase(Stream stream)
        {
            this.BaseStream = stream;
            
        }

        public override void Flush()
        {
            if (BaseStream != null)
                BaseStream.Flush();
        }

        public override void Close()
        {
            if (BaseStream != null)
                BaseStream.Close();
            
            base.Close();
        }

        #region Write Healpers
        // write int
        protected void writeInt32(int n)
        {
            byte[] intBuffer = BitConverter.GetBytes(n);
            BaseStream.Write(intBuffer, 0, 4);
        }
        #endregion

        #region Constant Properties
        public override bool CanRead
        {
            get { return false; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return true; }
        }
        #endregion

        #region Not Implimented


        public override long Length
        {
            get { throw new NotImplementedException(); }
        }

        public override long Position
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }
        #endregion

    }

    public class CryptoHelper
    {
        public static readonly int DefaultVersion = 3;
        public static readonly int MagicValue = 0x3a4b2c3d;

        public static readonly int KeyHashLength = 32;
        public static readonly int CryptoDataHashLength = 32;

        public static readonly int IterationCountMin = 10000;
        public static readonly int IterationCountMax = 10000 + 1000;

        public static void GetRandomSaltAndIterationCount(out int iterationCount, out byte[] salt)
        {
            byte[] iterationCountBytes = new byte[4];
            salt = new byte[32];

            RNGCryptoServiceProvider rnGenerator = new RNGCryptoServiceProvider();

            rnGenerator.GetBytes(iterationCountBytes);
            rnGenerator.GetBytes(salt);

            iterationCount = Math.Abs(System.BitConverter.ToInt32(iterationCountBytes, 0)) % (IterationCountMax - IterationCountMin);
            iterationCount += IterationCountMin;
        }

        public static byte[] ComputeKeyHash(byte[] key, int iterationCount, byte[] salt)
        {
            Rfc2898DeriveBytes passKey = new Rfc2898DeriveBytes(key, salt, iterationCount);
            return passKey.GetBytes(KeyHashLength);
        }

        public static AesManaged GetAesManeged(SecureString password, int iterationCount, byte[] salt)
        {
            IntPtr ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(password);
            string passwordString = System.Runtime.InteropServices.Marshal.PtrToStringUni(ptr);

            Rfc2898DeriveBytes passKey = new Rfc2898DeriveBytes(passwordString, salt);
            passKey.IterationCount = iterationCount;

            passwordString = null;

            // Gen Key and IV
            AesManaged AESCrypto = new AesManaged();
            AESCrypto.KeySize = 256;
            AESCrypto.Key = passKey.GetBytes(32);
            AESCrypto.IV = passKey.GetBytes(AESCrypto.BlockSize / 8);

            return AESCrypto;
        }


        public static bool CompareBytes(byte[] b1, byte[] b2)
        {
            if (b1 == null || b2 == null || b1.Length != b2.Length)
                return false;

            int i = 0;
            while (i < b1.Length && b1[i] == b2[i]) ++i;

            return i == b1.Length;
        }

    }

    public abstract class CryptoVersionEncoder : StreamWriterBase
    {
        protected SecureString password;

        public CryptoVersionEncoder(Stream stream, SecureString password)
            : base(stream)
        {
            this.password = password;
        }
    }

    public class CryptoVersionEncoder03 : CryptoVersionEncoder
    {

        bool isSetup;

        public CryptoVersionEncoder03(Stream stream, SecureString password)
            : base(stream, password)
        {
            setup();
        }

        private void setup()
        {
            byte[] salt;
            int iterationCount;


            /** 1.  KeyGen Paramiters: calculate and write **/
            CryptoHelper.GetRandomSaltAndIterationCount(out iterationCount, out salt);
            
            writeInt32(iterationCount);
            writeInt32(salt.Length);
            BaseStream.Write(salt, 0, salt.Length);

            /** 2.  KeyHash: generate and write **/
            AesManaged Ase = CryptoHelper.GetAesManeged(password, iterationCount, salt);
            byte[] passwordKeyHash = CryptoHelper.ComputeKeyHash(Ase.Key, iterationCount, salt);
            
            BaseStream.Write(passwordKeyHash, 0, passwordKeyHash.Length);
            
            /** 2.  Wrap output stream with compresor and encryptor **/
            CryptoStream cryptoStream = new CryptoStream(BaseStream, Ase.CreateEncryptor(), CryptoStreamMode.Write);
            GZipStream gzipStream = new GZipStream(cryptoStream, CompressionMode.Compress, false);

            BaseStream = gzipStream;

            isSetup = true;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (!isSetup)
                setup();

            BaseStream.Write(buffer, offset, count);
        }

        //CryptoStream cryptoStream; // temp - testing flush()
        //public override void Flush()
        //{
        //    encodedDataStream.Flush();
        //    //encodedDataStream.Close();

        //    if (cryptoStream != null)
        //    {
        //        cryptoStream.Flush();
        //        //cryptoStream.Close();
        //    }

        //    BaseStream.Flush();
            
        //}

        //public override void Close()
        //{
        //    this.Flush();

        //    encodedDataStream.Close();
        //    base.Close();
        //}

    }


    public abstract class StreamReaderBase : Stream
    {
        public Stream BaseStream { get; protected set; }
        protected static readonly int DefaultBufferLength = 64;//512;

        protected byte[] buffer;
        protected int bufferCount;
        protected int bufferPos;


        public StreamReaderBase(Stream stream) : this(stream, DefaultBufferLength) { }
        public StreamReaderBase(Stream stream, int bufferLength)
        {
            this.BaseStream = stream;

            if (bufferLength > 0)
                this.buffer = new byte[bufferLength];
        }

        public override void Close()
        {
            if (BaseStream != null)
                BaseStream.Close();
            base.Close();
        }

        #region Reading Helpers

        protected bool fillBuffer()
        {
            return fillBuffer(buffer.Length);
        }
        protected bool fillBuffer(int maxBytes)
        {
            Debug.Assert(bufferPos >= bufferCount);

            if (maxBytes > buffer.Length)
                buffer = new byte[Math.Max(maxBytes, buffer.Length * 2)];

            int bytesRead = BaseStream.Read(buffer, 0, maxBytes);

            bufferCount = bytesRead;
            bufferPos = 0;

            return bytesRead > 0;
        }


        protected bool readInt32(ref int intOut)
        {
            int bytesInBuffer = bufferCount - bufferPos;
            if (bytesInBuffer >= 4)
            {
                intOut = BitConverter.ToInt32(buffer, bufferPos);
                bufferPos += 4;
            }
            else if (bytesInBuffer == 0)
            {
                if (!fillBuffer()) return false;
                return readInt32(ref intOut);
            }
            else
            {
                byte[] intBuffer = new byte[4];
                if (!readBytes(intBuffer, 0, 4)) return false;

                intOut = BitConverter.ToInt32(intBuffer, 0);
            }

            return true;
        }

        protected bool readBytes(byte[] dest, int offset, int count)
        {

            int bytesInBuffer = bufferCount - bufferPos;
            if (bytesInBuffer >= count)
            {
                System.Buffer.BlockCopy(buffer, bufferPos, dest, offset, count);
                bufferPos += count;
            }
            else if (bytesInBuffer == 0)
            {
                if (!fillBuffer()) return false;
                return readBytes(dest, offset, count);
            }
            else
            {
                System.Buffer.BlockCopy(buffer, bufferPos, dest, offset, bytesInBuffer);
                bufferPos += bytesInBuffer;

                if (!fillBuffer()) return false;

                int bytesRemaing = count - bytesInBuffer;
                bytesInBuffer = bufferCount - bufferPos;

                if (bytesInBuffer >= bytesRemaing)
                {
                    System.Buffer.BlockCopy(buffer, bufferPos, dest, offset + count - bytesRemaing, bytesRemaing);
                    bufferPos += bytesRemaing;
                }
                else
                {
                    return false;
                }
            }

            return true;
        }

        #endregion

        #region Constant Properties

        public override bool CanRead
        {
            get { return true; }
        }

        public override bool CanSeek
        {
            get { return false; }
        }

        public override bool CanWrite
        {
            get { return false; }
        }

        #endregion

        #region Unsuported

        public override void Flush()
        {
            throw new NotImplementedException();
        }

        public override long Length
        {
            get { throw new NotImplementedException(); }
        }

        public override long Position
        {
            get
            {
                throw new NotImplementedException();
            }
            set
            {
                throw new NotImplementedException();
            }
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            throw new NotImplementedException();
        }

        public override void SetLength(long value)
        {
            throw new NotImplementedException();
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            throw new NotImplementedException();
        }

        #endregion
    }


    public abstract class CryptoVersionDecoder : StreamReaderBase
    {
        public virtual int Version { get { return 3; } }

        protected SecureString password;

        public CryptoVersionDecoder(Stream stream, SecureString password) : this(stream, password, DefaultBufferLength) { }
        public CryptoVersionDecoder(Stream stream, SecureString password, int bufferLength)
            : base(stream, bufferLength)
        {
            this.password = password;
        }

    }

    public class CryptoVersionDecoder03 : CryptoVersionDecoder
    {
        // key generation
        protected int iterationCount;
        protected int saltLength;
        protected byte[] salt;

        // key hash
        protected byte[] keyHash;

        protected bool isSetup;

        public CryptoVersionDecoder03(Stream stream, SecureString password) : this(stream, password, DefaultBufferLength) { }
        public CryptoVersionDecoder03(Stream stream, SecureString password, int bufferLength)
            : base(stream, password, bufferLength)
        {
            setup();
        }

        protected virtual void setup()
        {
            /** 1.  Reade Header Fields **/
            if (!fillBuffer(4 * 2)) goto incompleteHeader;  // get bytes for: iter count + salt length 

            // iteration count
            if (!readInt32(ref iterationCount)) goto incompleteHeader;

            // salt length
            if (!readInt32(ref saltLength)) goto incompleteHeader;

            if (!fillBuffer(saltLength)) goto incompleteHeader;  // get bytes for: salt bytes

            // salt bytes
            salt = new byte[saltLength];
            if (!readBytes(salt, 0, saltLength)) goto incompleteHeader;

            if (!fillBuffer(CryptoHelper.KeyHashLength)) goto incompleteHeader;  // get bytes for: key hash bytes 

            // key hash
            keyHash = new byte[CryptoHelper.KeyHashLength];
            if (!readBytes(keyHash, 0, CryptoHelper.KeyHashLength)) goto incompleteHeader;


            /** 2.  Generate KeyHash from password and compare to file version **/

            // generate 
            AesManaged Ase = CryptoHelper.GetAesManeged(password, iterationCount, salt);
            byte[] PasswordKeyHash = CryptoHelper.ComputeKeyHash(Ase.Key, iterationCount, salt);

            // compare
            if (!CryptoHelper.CompareBytes(keyHash, PasswordKeyHash))
                throw new Exception("Password did not match");


            /** 3.  Wrap source stream: Decrypt and Deconpress **/
            CryptoStream cryptoStream = new CryptoStream(BaseStream, Ase.CreateDecryptor(), CryptoStreamMode.Read);
            GZipStream gzipStream = new GZipStream(cryptoStream, CompressionMode.Decompress);

            BaseStream = gzipStream;

            isSetup = true;
            return;

        incompleteHeader:
            throw new Exception("Samcrypt header incompleate");
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (!isSetup)
                setup();

            return BaseStream.Read(buffer, offset, count);
        }
    }

    public class LimitStreamReader : StreamReaderBase
    {
        int byteCountLimit;
        int byteCountRemaining;

        public LimitStreamReader(Stream stream, int byteCountLimit)
            : base(stream, 8)
        {
            this.byteCountLimit = byteCountLimit;
            this.byteCountRemaining = byteCountLimit;
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            int bytesRead;
            if (byteCountRemaining - count > 0)
            {
                bytesRead = BaseStream.Read(buffer, offset, count);
                byteCountRemaining -= count;
            }
            else if (byteCountRemaining > 0)
            {
                bytesRead = BaseStream.Read(buffer, offset, byteCountRemaining);
                byteCountRemaining = 0;
            }
            else
            {
                bytesRead = 0;
            }
            return bytesRead;
        }
    }

    public class CryptoVersionDecoder02 : CryptoVersionDecoder03
    {
        protected int cryptoDataLength;

        public override int Version { get { return 2; } }
        
        public CryptoVersionDecoder02(Stream stream, SecureString password) : base(stream, password) { }
        
        protected override void setup()
        {
            /** 1.  Reade Header Fields **/
            if (!fillBuffer(4 * 2)) goto incompleteHeader;  // get bytes for: iter count + salt length 

            // iteration count
            if (!readInt32(ref iterationCount)) goto incompleteHeader;

            // salt length
            if (!readInt32(ref saltLength)) goto incompleteHeader;

            if (!fillBuffer(saltLength)) goto incompleteHeader;  // get bytes for: salt bytes

            // salt bytes
            salt = new byte[saltLength];
            if (!readBytes(salt, 0, saltLength)) goto incompleteHeader;

            if (!fillBuffer(CryptoHelper.KeyHashLength)) goto incompleteHeader;  // get bytes for: key hash bytes 

            // key hash
            keyHash = new byte[CryptoHelper.KeyHashLength];
            if (!readBytes(keyHash, 0, CryptoHelper.KeyHashLength)) goto incompleteHeader;

            // EData length
            if (!fillBuffer(4)) goto incompleteHeader;
            if (!readInt32(ref cryptoDataLength)) goto incompleteHeader;
            

            /** 2.  Generate KeyHash from password and compare to file version **/

            // generate 
            AesManaged Ase = CryptoHelper.GetAesManeged(password, iterationCount, salt);
            byte[] PasswordKeyHash = CryptoHelper.ComputeKeyHash(Ase.Key, iterationCount, salt);

            // compare
            if (!CryptoHelper.CompareBytes(keyHash, PasswordKeyHash))
                throw new Exception("Password did not match");


            /** 3.  Wrap source stream: Decrypt and Deconpress **/
            LimitStreamReader limitStream = new LimitStreamReader(BaseStream, cryptoDataLength);
            CryptoStream cryptoStream = new CryptoStream(limitStream, Ase.CreateDecryptor(), CryptoStreamMode.Read);
            GZipStream gzipStream = new GZipStream(cryptoStream, CompressionMode.Decompress);

            BaseStream = gzipStream;

            isSetup = true;
            return;

        incompleteHeader:
            throw new Exception("Samcrypt header incompleate");
        }
    }

    public sealed class CryptoVersionDecoder01_INSECURE : CryptoVersionDecoder02  
    {
        public override int Version { get { return 1; } }

        static readonly int CryptoDataHashLength = 32;
        byte[] cryptoDataHash;

        public CryptoVersionDecoder01_INSECURE(Stream stream, SecureString password) : base(stream, password) { }

        private byte[] computeHash_Insecure(byte[] bytes)
        {
            return new SHA256Managed().ComputeHash(bytes);
        }

        protected override void setup()
        {
            /** 0.  Get Key Gen Paramiters **/
            iterationCount = 10000;
            salt = System.Text.Encoding.Default.GetBytes("snowsalt"); /** INSECURE - DEPRICATED **/
            saltLength = salt.Length;

            /** 1.  Reade Header Fields **/
            
            // key hash
            if (!fillBuffer(CryptoHelper.KeyHashLength)) goto incompleteHeader;  // get bytes for: key hash bytes 
            keyHash = new byte[CryptoHelper.KeyHashLength];
            if (!readBytes(keyHash, 0, CryptoHelper.KeyHashLength)) goto incompleteHeader;

            // EData Hash
            if (!fillBuffer(CryptoDataHashLength)) goto incompleteHeader;
            cryptoDataHash = new byte[CryptoDataHashLength];
            if (!readBytes(cryptoDataHash, 0, CryptoDataHashLength)) goto incompleteHeader;

            // EData length
            if (!fillBuffer(4)) goto incompleteHeader;
            if (!readInt32(ref cryptoDataLength)) goto incompleteHeader;


            /** 2.  Generate KeyHash from password and compare to file version **/

            // generate 
            AesManaged Ase = CryptoHelper.GetAesManeged(password, iterationCount, salt);
            byte[] PasswordKeyHash = computeHash_Insecure(Ase.Key); /** INSECURE - DEPRICATED **/

            // compare
            if (!CryptoHelper.CompareBytes(keyHash, PasswordKeyHash))
                throw new Exception("Password did not match");
            
            /** 3.  Wrap source stream: Decrypt and Deconpress **/
            CryptoStream cryptoStream = new CryptoStream(BaseStream, Ase.CreateDecryptor(), CryptoStreamMode.Read);
            GZipStream gzipStream = new GZipStream(cryptoStream, CompressionMode.Decompress);

            BaseStream = gzipStream;

            isSetup = true;
            return;

        incompleteHeader:
            throw new Exception("Samcrypt header incompleate");
        }
    }


    public class CryptoEncoder : StreamWriterBase
    {
        
        SecureString password;
        int version;

        bool isSetup;

        public CryptoEncoder(Stream stream, SecureString password) : this(stream, password, CryptoHelper.DefaultVersion) { }
        public CryptoEncoder(Stream stream, SecureString password, int version)
            : base(stream)
        {
            this.password = password;
            this.version = version;

            setup();
        }

        private void setup()
        {
            writeInt32(CryptoHelper.MagicValue);
            writeInt32(version);

            Stream encoderStream = null;

            switch (version)
            {
                case 1:
                    throw new Exception("Incompatible Samcrypt Data Version (too old)");
                    
                case 2:
                    throw new Exception("Incompatible Samcrypt Data Version (too old)");
                    
                case 3:
                    encoderStream = new CryptoVersionEncoder03(BaseStream, password);
                    break;
                default:
                    throw new Exception("Incompatible Samcrypt Data Version (too new)");

            }

            BaseStream = encoderStream;
            isSetup = true;

        }
        
        public override void Write(byte[] buffer, int offset, int count)
        {
            if (!isSetup)
                setup();

            BaseStream.Write(buffer, offset, count);
        }

    }


    public class CryptoDecoder : StreamReaderBase
    {

        SecureString password;
        bool isSetup;

        public int Version
        {
            get
            {
                if (!isSetup)
                    setup();

                CryptoVersionDecoder baseDecoder = BaseStream as CryptoVersionDecoder;

                return baseDecoder.Version;
            }
        }

        public CryptoDecoder(Stream stream, SecureString password)
            : base(stream)
        {
            this.password = password;

            setup();
        }

        private void setup()
        {
            if (!fillBuffer(2 * 4)) goto incompleteHeader;  // get bytes for: magic and version

            int magic = 0;
            if (!readInt32(ref magic)) goto incompleteHeader;

            if (magic != CryptoHelper.MagicValue)
                throw new Exception("Not Samcrypt data");

            int version = 0;
            if (!readInt32(ref version)) goto incompleteHeader;

            Stream decoderStream;

            switch (version)
            {
                case 1:
                    decoderStream = new CryptoVersionDecoder01_INSECURE(BaseStream, password);
                    break;
                case 2:
                    decoderStream = new CryptoVersionDecoder02(BaseStream, password);
                    break;
                case 3:
                    decoderStream = new CryptoVersionDecoder03(BaseStream, password);
                    break;
                default:
                    throw new Exception("Incompatible Samcrypt Data Version");

            }

            BaseStream = decoderStream;

            isSetup = true;
            return;

        incompleteHeader:
            throw new Exception("Incomplete Header Data");
        }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (!isSetup)
                setup();

            return BaseStream.Read(buffer, offset, count);
        }

    }


}
